Release 10.1A: OpenEdge Development:
Progress Dynamics Advanced Development


Defining the manager itself

In the AppBuilder, select New Structured Manager to create the new manager procedure from its template. Enter the standard file documentation information in the wizard. Name the procedure fldedsrvrp.p. Note that this server-side manager procedure in fact contains all the code for both the client and server versions of the manager. As you write code for the manager, the DB-REQUIRED flag determines which code gets compiled into which version of the manager.

To create the client-side manager procedure, simply copy the client manager template, which is the file af/sup2/aftemclntp.p, to the working directory where you want your manager. Name your version fldedclntp.p. Edit the file to include the server manager procedure fldedsrvrp.p, including its relative pathname, depending on your own directory structure, as shown:

/* fldedclntp.p - non-db proxy for fldedsrvrp.p */ 
&GLOBAL-DEFINE DB-REQUIRED FALSE 
{"fldedsrvrp.p"} 

As you can see, the client procedure simply defines the DB-REQUIRED flag to be false and then includes the principal (server) manager procedure. In this way, the references to DB-REQUIRED inside fldedsrvrp.p determine which code gets compiled out of the client procedure. Save the client manager procedure and compile it when you have finished writing the manager. All your code now goes into fldedsrvrp.p.

Data caching considerations

The first procedure to define in the manager, cacheFieldEdits, builds a client-side cache of data from the Entity Field Edit table.

Different kinds of managers might need to cache different data on either the client or server or both. Keep the following things in mind when you’re designing the caching mechanism for a manager:

In the case of the example Field Edit Manager, data caching makes sense only on the client. Each time an SDO starts up, it requests the field edit data for its enabled table or tables from the Field Edit Manager. If that data isn’t already available, then the client manager requests it from the server manager and adds it to the client cache.

The cache temp-table has the following definition:

DEFINE TEMP-TABLE ttFieldEditData NO-UNDO 
    FIELD cEntityName          AS CHARACTER 
    FIELD cFieldName           AS CHARACTER 
    FIELD cEditType            AS CHARACTER 
    FIELD cEditValue           AS CHARACTER 
    INDEX key1 AS UNIQUE PRIMARY cEntityName cFieldName cEditType. 

The Entity Name comes from the Entity Mnemonic table, and the Field name from the Display Field table. The Edit Type and Edit Value come from the new Field Edit table.

The cacheFieldEdits procedure takes a list of one or more tables as input, and checks to see whether the field edit data for the tables) is already in the cache temp-table. Note that because the procedure will normally be called on the client, the section editor’s DB-Required toggle box must be checked off so that the code is compiled into the client version of the manager, as shown in Figure 7–5.

Figure 7–5: Section Editor

If any of the tables are not yet cached, then the code calls a separate procedure that has the DB-Required flag set to TRUE to load them from the Repository database. The cacheFieldEdits procedure must check whether the procedure that reads the database is available to run locally or must be run remotely, so that it knows where to run the database-dependent code. Because the DB-Required procedure is completely compiled out of the client manager when the manager is split between client and server, the code can check for the existence of the DB-Required procedure in the manager’s internal entries. If it is not there, then it must be run remotely. This code fragment uses the include file dynlaunch.i to make the call, which is explained in the following example:

 IF cTablesToRead NE '':U THEN DO: 
    IF LOOKUP('fetchFieldEdits', THIS-PROCEDURE:INTERNAL-ENTRIES) NE 0 THEN 
        RUN fetchFieldEdits (INPUT pcTableList,  
                             OUTPUT TABLE ttNewData). 
    ELSE {dynlaunch.i 
            &PLIP   = 'FieldEditManager' 
            &iProc  = 'fetchFieldEdits' 
            &mode1  = INPUT  &parm1 = pcTableList &dataType1 = CHARACTER 
            &mode2  = OUTPUT &parm2 = ttNewData   &dataType2 = TABLE 
         } 

An alternative way to make this check is to use the following statement:

IF CONNECTED(‘ICFDB’) THEN 

This has the disadvantage of hard-coding the logical database name where the data resides into the procedure, which might not be a good idea. If you later move the data (to your application database, for example) then you must remember to change this statement as well.

Using dynlaunch.i to make server calls in your manager

The include file dynlaunch.i supports making a call to an internal procedure inside a server-side procedure, which might or might not already be running. It uses the Progress 4GL dynamic Call object, which is new to Progress Version 9.1D, to package the parameters to the procedure call. It handles all these steps in a single AppServer call:

As a result, using dynlaunch.i is generally the preferred way to make calls to internal procedures across the AppServer connection in Progress Dynamics Version 2, as long as the external procedure on the server does need to be started and then left running after the call is complete.

These are the basic include file arguments for dynlaunch.i:

There must be three named arguments for each parameter in the internal procedure’s calling sequence. For each of these, the letter n represents the order of the parameters in the call:

As with all include file references, quoted strings must be inside single quotes. If a string argument includes spaces or other word breaks, then the single-quoted string must then be inside double quotes.

In the case of this example, the code is running the internal procedure fetchFieldEdits in the server-side FieldEditManager, and passing as an INPUT parameter a list of the tables to retrieve edit information for, and returning as an OUTPUT parameter a temp-table containing those edits.

Writing the server-side caching procedure

The procedure that reads database data into the temp-table is separate from cacheFieldEdits because it must have the Db-Required toggle box set to TRUE, so that it will be compiled only on the server-side, or when the database is otherwise available.

This fetchFieldEdits procedure uses a copy of the ttFieldEditData temp-table that must also be defined in the Definitions section of the manager, as shown below:

DEFINE TEMP-TABLE ttNewData NO-UNDO LIKE ttFieldEditData. 

This is so that only the data for the current request is returned in the call. The client-side cache will gradually be built up as more requests are made. Because no data is maintained in memory on the server, fetchFieldEdits first needs to empty out any leftover data from an earlier request. For example:

Procedure fetchFieldEdits: 
/*------------------------------------------------------------------------- 
  Purpose:     Server-side procedure to load field edit data from the database 
               into a temp-table, to be returned to the client. 
  Parameters:  INPUT  PARAMETER pcTableList AS CHARACTER  NO-UNDO --  
                         list of tables to cache. 
               OUTPUT PARAMETER TABLE FOR ttNewData --  
                         returned temp-table. 
  Notes:       This DB-REQUIRED procedure is executed on the server, from the  
               client, if the Manager is divided between client and server.  
               It only loads newly requested data into the NewData temp-table,  
               and returns that to be appended to the client cache. 
-------------------------------------------------------------------------*/ 
DEFINE INPUT  PARAMETER pcTableList AS CHARACTER  NO-UNDO. 
DEFINE OUTPUT PARAMETER TABLE FOR ttNewData. 
DEFINE VARIABLE iNum AS INTEGER    NO-UNDO. 
/* Remove any leftover data from a previous request. The ttNewData table 
       just holds the data for the current request and returns that to the 
client. */   
    EMPTY TEMP-TABLE ttNewData. 

The procedure then goes through the list of requested tables, locates any FieldEdit records for their fields, and creates temp-table records for them, as shown:

/* Read field edit data from database into the temp-table to return 
   to the caller. */ 
      DO iNum = 1 TO NUM-ENTRIES(pcTableList): 
        FIND FIRST gsc_entity_mnemonic WHERE  
            gsc_entity_mnemonic.entity_mnemonic_description =  
              ENTRY(iNum, pcTableList) NO-LOCK NO-ERROR. 
        IF AVAILABLE gsc_entity_mnemonic THEN DO: 
          FOR EACH gsc_entity_display_field OF gsc_entity_mnemonic, 
              EACH gsc_entity_field_edit OF gsc_entity_display_field NO-LOCK: 
             
              CREATE ttNewData. 
              ASSIGN  
                ttNewData.cEntityName =  
                  gsc_entity_mnemonic.entity_mnemonic_description 
                ttNewData.cFieldName  =  
                  gsc_entity_display_field.DISPLAY_field_name 
                ttNewData.cEditType   = gsc_entity_field_edit.edit_type 
                ttNewData.cEditValue  = gsc_entity_field_edit.edit_value. 
          END.  /* for each entity field edit of each display field */ 
        END.  /* if available entity */ 
      END.  /* do to number of tables */ 
      RETURN. 
END PROCEDURE. 

Back in the calling procedure, cacheFieldEdits, this data is copied into the client cache, as shown:

FOR EACH ttNewData: 
            CREATE ttFieldEditData. 
            BUFFER-COPY ttNewData TO ttFieldEditData. 
        END. 

Note that it would be slightly more efficient to use the APPEND keyword with the OUTPUT parameter and have the new data added directly to the existing client cache. However, the underlying mechanism that supports dynlaunch.i does not allow this, so this is a penalty paid when using it. Keep in mind that in more complex caching examples, it is likely that there would be some overlap between data returned and data already on the client that couldn’t be checked in advance. For example, if you were to use the Repository Manager API to retrieve object definitions to cache on the client, then even when the parent object was uniquely identifiable, a single request could return a whole host of data for various object instances contained in that parent object, some of which might already be present in the client cache.

In such a case you would have to receive new data in a separate table or set of tables anyway, and then do the necessary work on the client side to determine whether any of the data was already present, to avoid potential clashes of unique object IDs or other keys.

Clearing the client cache

In order to clear the cache on the client side if it must be refreshed, you can define a clearClientCache procedure. This checks whether it is being invoked on the server or not, and empties the temp-table if it is on the client. The DB-Required toggle box must be checked off for this procedure, as shown:

Procedure clearClientCache: 
/*------------------------------------------------------------------------- 
  Purpose:     This procedure clears the cache of field edit data. 
  Parameters:  <none> 
  Notes:        
-------------------------------------------------------------------------*/ 
   
  IF NOT (SESSION:REMOTE OR SESSION:CLIENT-TYPE = "WEBSPEED":U) THEN 
  DO: 
    EMPTY TEMP-TABLE ttFieldEditData. 
  END. 
END PROCEDURE. 

In this way, any further requests of the cache on the client side will force a request to the server, since no cached data will be found on the client.

Note the use of the syntax, as shown:

IF [NOT] (SESSION:REMOTE OR SESSION:CLIENT-TYPE = "WEBSPEED") THEN… 

This expression tells the code whether it is executing on the server-side of a connection that has a separate client. Only if this is not the case do you want to empty the cache temp-table, because it is not maintained on the server.

Note: In this example, you could safely empty the temp-table on the server as well, because nothing is maintained there anyway. However, in cases where data is cached separately on client and server, it could make a big difference which side you empty the cache on.

The NOT keyword is included in the statement to make it TRUE for the client side (including a stand-alone client with a local database connection) or omitted to make it TRUE for the server side. The SESSION:REMOTE attribute is true if this is an AppServer session. Otherwise, the CLIENT-TYPE attribute will be equal to WEBSPEED if the session is a WebSpeed Agent. In either of these cases, the code is executing remotely relative to the user interface and presumes to have a database connection to the Repository data.

Note that this expression is a little different in its effect from the earlier check of the INTERNAL-ENTRIES, or for that matter making a CONNECTED database check. In the case where the client and server are effectively combined, that is, when the client session is running the full server-side manager with a database connection, none of the INTERNAL-ENTRIES or CONNECTED checks will return TRUE because the full manager is running and the database is connected. But the NOT SESSION:REMOTE check will also return TRUE because it is a not a remote session. This is as it should be, since a single session is doing the work of both client and server. Think about which of these types of checks you want to use in a particular situation, depending on the logic of the code being bracketed by the expression.

Retrieving field edit data on the client

Now that you’ve taken care of getting field edit data cached on the client, you need a procedure to do lookups in it for client-side objects that need the information. You can do this with a procedure called getFieldEditData. This takes input parameters for the table, field, and one or more types of edit types the caller needs values for, and returns a delimited list of the values. On the chance that the value for some new Edit Type might itself contain a comma, the Edit Values OUTPUT parameter uses CHR(3) as a delimiter between the values for the requested edit types.

The procedure simply looks up the requested records in the temp-table and returns their values, returning blank for edit types not defined for the fields in the temp-table. Note that the code presumes that the data is already available in the client cache, because the SDO for the table will have requested it. If this isn’t reliably the case (perhaps because clearClientCache might have been called after the SDO was initialized), this procedure could run cacheFieldEdits itself if the data needed isn’t available.

Remember to check the DB-Required toggle box off for this procedure:

Procedure fieldEditData: 
/*------------------------------------------------------------------------- 
  Purpose:     Returns field edit data for a database field.  It can return 
               edit values for one or more edit types. 
  Parameters:  INPUT  pcTable      AS CHARACTER - Table name 
               INPUT  pcField      AS CHARACTER - Field name 
               INPUT  pcEdittypes  AS CHARACTER - Edit types 
                                               can be a comma-delimited list 
               OUTPUT pcEditValues AS CHARACTER - Edit Values 
                                               will be a CHR(3)-delimited list  
                                               for multiple edit types 
  Notes:        
------------------------------------------------------------------------*/ 
DEFINE INPUT  PARAMETER pcTable      AS CHARACTER  NO-UNDO. 
DEFINE INPUT  PARAMETER pcField      AS CHARACTER  NO-UNDO. 
DEFINE INPUT  PARAMETER pcEdittypes  AS CHARACTER  NO-UNDO. 
DEFINE OUTPUT PARAMETER pcEditValues AS CHARACTER  NO-UNDO. 
DEFINE VARIABLE itypes AS INTEGER    NO-UNDO. 
  DO itypes = 1 TO NUM-ENTRIES(pcEdittypes): 
    FIND ttFieldEditData WHERE  
        ttFieldEditData.cEntityName = pcTable AND 
        ttFieldEditData.cFieldName  = pcField AND 
        ttFieldEditData.cEditType   = ENTRY(itypes, pcEdittypes) NO-LOCK 
NO-ERROR. 
    pcEditValues = pcEditValues +  
        (IF itypes > 1 THEN CHR(3) ELSE '':U) +           
          IF AVAILABLE ttFieldEditData THEN 
            ttFieldEditData.cEditValue ELSE '':U. 
  END.  /* DO 1 to number of edit type entries */ 
  RETURN. 
END PROCEDURE. 


Copyright © 2005 Progress Software Corporation
www.progress.com
Voice: (781) 280-4000
Fax: (781) 280-4095